package com.virtual.repository.common.managers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.springframework.util.StringUtils;

import com.virtual.repository.common.Constants;
import com.virtual.repository.common.criteria.Criterion;
import com.virtual.repository.common.model.CollectionProperty;
import com.virtual.repository.common.model.Mapping;
import com.virtual.repository.common.model.ModelConfiguration;
import com.virtual.repository.common.model.Property;
import com.virtual.repository.common.queries.Query;
import com.virtual.repository.common.queries.ResultEntity;
import com.virtual.repository.utils.ReflectionUtil;

/**
 * Class responsible for translating the entire query to the entity's names 
 * and parsing the result entities to the object's names.
 * @author kchaber
 *
 */
public class QueryParser implements IQueryParser {
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public <TYPE extends Object> TYPE parseSingleResult(ResultEntity resultEntity, ModelConfiguration modelConfig, Class<TYPE> expectedClass) throws Exception {
		TYPE resultObject = null;

		resultObject = expectedClass.newInstance();
		Mapping mapping = modelConfig.getMapping(expectedClass.getName());

		Iterator<Entry<String, Object>> it = resultEntity.getEntityFields().entrySet().iterator();
		while (it.hasNext()) {
			Entry<String, Object> currEntry = it.next();

			if (currEntry.getValue() instanceof Map<?, ?>) {
				// parse nested object
				Map<String, Object> nestedObjectValues = (Map<String, Object>) currEntry.getValue();

				Property nestedPropertyData = mapping.getProperty(currEntry.getKey());
				Class<?> nestedObjectClass = Class.forName(nestedPropertyData.getClassName());

				ResultEntity nestedEntity = new ResultEntity();
				nestedEntity.setEntityFields(nestedObjectValues);

				// recursive invocation
				Object nestedObject = this.parseSingleResult(nestedEntity, modelConfig, nestedObjectClass);

				Mapping nestedObjectMapping = modelConfig.getMapping(currEntry.getKey());
				ReflectionUtil.setField(resultObject, nestedObjectMapping.getClassName(), nestedObject);
			} else {
				// parse single class field
				Property property = mapping.getProperty(currEntry.getKey());
				if (property != null) {
					ReflectionUtil.setField(resultObject, property.getName(), currEntry.getValue());
				} else {
					System.out.println("WARN: The driver returned some additional field: " + currEntry.getKey() + " for expected class: " + expectedClass);
				}
			}
		}

		return resultObject;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	//TODO: Translation of the selection fields, criteria and order by clauses can be performed concurrently
	public Query translateQuery(Query query, ModelConfiguration modelConfig) throws Exception {
		if (query == null) {
			throw new IllegalStateException("Query cannot be null");
		}
		
		Mapping parentMapping = modelConfig.getMapping(query.getSelectedClass().getName());
		if (parentMapping == null) {
			throw new IllegalStateException("Mapping for class: " + query.getSelectedClass().getName() + " not found");
		}
		
		//rewrite selection fields
		List<String> selectionFields = this.translateSelectionFields(query.getSelectedFields(), parentMapping, modelConfig);
		
		//rewrite selection criteria
		List<Criterion> selectionCriteria = this.translateSelectionCriteria(query.getSelectionCriteria(), parentMapping, modelConfig);
		
		//rewrite order by clauses
		Map<String, Boolean> orderByCluases = this.translateOrderByClauses(query.getOrderByMap(), parentMapping, modelConfig);
		
		//construct the adjusted(translated) query
		Query adjustedQuery = new Query(query.getSelectedClass(), selectionFields, selectionCriteria, query.getLimit(), orderByCluases, parentMapping.getDbEntityName());
		
		System.out.println("Translated query: " + adjustedQuery);
		return adjustedQuery;
	}
	
	/**
	 * Translates the selection fields, given by the class field's names, to the names in the entity/table defined in the database. 
	 * @param fieldsToTranslate the list of the selection fields
	 * @param parentMapping the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param modelConfig the configuration model
	 * @return
	 */
	protected List<String> translateSelectionFields(List<String> fieldsToTranslate, Mapping parentMapping, ModelConfiguration modelConfig) {
		List<String> translatedFields = new ArrayList<>();

		//if selection fields is empty or null then we need to translate all available fields
		if (fieldsToTranslate == null || fieldsToTranslate.isEmpty()) {
			translatedFields = this.getAllTranslatedFieldsToSelect("", parentMapping, modelConfig);
		} else {
			for (String fullFieldName : fieldsToTranslate) {
				translatedFields.addAll(this.translateFieldName(fullFieldName, parentMapping, modelConfig));
				
			}
		}

		return translatedFields;
	}
	
	/**
	 * Translates the field's names, defined in the criteria, 
	 * given by the class field's names, to the names in the entity/table defined in the database. 
	 * @param criteriaToTranslate the list of the criteria to translate
	 * @param parentMapping the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param modelConfig the configuration model
	 * @return
	 * @throws Exception
	 */
	protected List<Criterion> translateSelectionCriteria(List<Criterion> criteriaToTranslate, Mapping parentMapping, ModelConfiguration modelConfig) throws Exception {
		List<Criterion> translatedCriteria = new ArrayList<>();

		for (Criterion criterion : criteriaToTranslate) {
			String translatedName = this.translateFieldName(criterion.getProperty(), parentMapping, modelConfig).get(0);
			
			Criterion newCriterion = criterion.clone();
			newCriterion.setProperty(translatedName);
			
			translatedCriteria.add(newCriterion);
		}
		
		return translatedCriteria;
	}
	
	/**
	 * Translates the field's names, defined in the order by clauses, 
	 * given by the Java class field's names to the names in the entity/table defined in the database. 
	 * @param orderByClausesToTranslate the map containing order by clauses to translate
	 * @param parentMapping the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param modelConfig the configuration model
	 * @return
	 */
	protected Map<String, Boolean> translateOrderByClauses(Map<String, Boolean> orderByClausesToTranslate, Mapping parentMapping, ModelConfiguration modelConfig) {
		Map<String, Boolean> translatedOrderByClauses = new HashMap<>();

		Map<String, Boolean> orderByCluases = new HashMap<>();
		Iterator<Entry<String, Boolean>> orderByIt = orderByClausesToTranslate.entrySet().iterator();
		while(orderByIt.hasNext()) {
			Entry<String, Boolean> currEntry = orderByIt.next();
			String translatedFieldName = this.translateFieldName(currEntry.getKey(), parentMapping, modelConfig).get(0);
			
			orderByCluases.put(translatedFieldName, currEntry.getValue());
		}
		
		return translatedOrderByClauses;
	}
	
	/**
	 * Translates the single field name from the Java class field's names to
	 * the names in the entity/table, defined in the database.
	 * @param fullFieldName the name of the class's field
	 * @param parentMapping the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param modelConfig the configuration model
	 * @return
	 */
	private List<String> translateFieldName(String fullFieldName, Mapping parentMapping, ModelConfiguration modelConfig) {
		List<String> results = new ArrayList<>();
		
		String translatedName = "";
		Mapping currMapping = parentMapping;
		
		//complex fields are separated using declared separator
		//example: customer.lawn.sum
		//		   customer, lawn are objects
		//		   sum is a property inside the Lawn object
		String[] singleFieldNames = fullFieldName.split(Constants.NESTED_FIELDS_SEPARATOR_REGEX);
		for (int i = 0; i < singleFieldNames.length; i++) {
			String singleName = singleFieldNames[i];
			Property prop = currMapping.getProperty(singleName);
			
			if (i >= singleFieldNames.length - 1) {
				if (modelConfig.getMappings().containsKey(prop.getClassName())) {
					Mapping newParentMapping = modelConfig.getMapping(prop.getClassName());
					String prefix = newParentMapping.getDbEntityName() + Constants.NESTED_FIELDS_SEPARATOR;
					
					List<String> nestedFields = this.getAllTranslatedFieldsToSelect(prefix, newParentMapping, modelConfig);
					results.addAll(nestedFields);
				} else {
					//the last element is a property, in above example: sum
					translatedName += prop.getSourceName();
				}
			} else {
				//translate the entity, in above example: customer, lawn
				currMapping = modelConfig.getMapping(prop.getClassName());
				translatedName += currMapping.getDbEntityName() + Constants.NESTED_FIELDS_SEPARATOR;
			}
		}
		
		if (!StringUtils.isEmpty(translatedName)) {
			results.add(translatedName);
		}
		
		return results;
	}
	
	/**
	 * Recursive function. Returns the list of the all available selection fields.
	 * Invoked in @see com.virtual.repository.common.managers.QueryParser#translateSelectionFields(List<String>, Mapping, ModelConfiguration)
	 * when the entire query has not specified any selection field.
	 * 
	 * @param parentMapping the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param modelConfig the configuration model
	 * @return
	 */
	private List<String> getAllTranslatedFieldsToSelect(String prefix, Mapping parentMapping, ModelConfiguration modelConfig) {
		//set for removing duplicates
		Set<String> selectionFields = new HashSet<>();
		
		Iterator<Entry<String, Property>> it = parentMapping.getProperties().entrySet().iterator();
		while(it.hasNext()) {
			Entry<String, Property> currEntry = it.next();
			Property prop = currEntry.getValue();
			
			if (!modelConfig.getMappings().containsKey(prop.getClassName())) {
				selectionFields.add(prefix + prop.getSourceName());
			} else {
				Mapping newParent = modelConfig.getMapping(prop.getClassName());
				String newPrefix = prefix + newParent.getDbEntityName() + Constants.NESTED_FIELDS_SEPARATOR;
				selectionFields.addAll(this.getAllTranslatedFieldsToSelect(newPrefix, newParent, modelConfig));
			}
		}
		selectionFields.addAll(this.getAllTranslatedCollectionFieldsToSelect(prefix, parentMapping, modelConfig));
		
		return new ArrayList<>(selectionFields);
	}
	
	private List<String> getAllTranslatedCollectionFieldsToSelect(String prefix, Mapping parentMapping, ModelConfiguration modelConfig) {
		//set for removing duplicates
		Set<String> selectionFields = new HashSet<>();
		
		if (parentMapping.getCollections() != null) {
			Iterator<Entry<String, CollectionProperty>> it = parentMapping.getCollections().entrySet().iterator();
			while (it.hasNext()) {
				Entry<String, CollectionProperty> currEntry = it.next();
				CollectionProperty collProp = currEntry.getValue();

				if (modelConfig.getMappings().containsKey(collProp.getPropertyClass())) {
					Mapping newParent = modelConfig.getMapping(collProp.getPropertyClass());
					String newPrefix = prefix + newParent.getDbEntityName() + Constants.NESTED_FIELDS_SEPARATOR;
					selectionFields.addAll(this.getAllTranslatedFieldsToSelect(newPrefix, newParent, modelConfig));
				}
			}
		}
		
		return new ArrayList<>(selectionFields);
	}
}
